package gui.dialog;

import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.net.InterfaceAddress;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JFormattedTextField;
import javax.swing.JFormattedTextField.AbstractFormatter;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JCheckBox;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.border.EtchedBorder;
import javax.swing.text.NumberFormatter;

import controller.MessageController;
import controller.UserAlreadyConnected;

import network.AbstractNetworkInterface;
import network.config.BadPortException;
import network.config.NetworkConfigObserver;
import network.config.NetworkConfiguration;
import network.config.NetworkConfiguration.FeatureType;
import network.config.NetworkConfiguration.PortType;
import network.config.NetworkConfiguration.Protocol;



public class ChatConfigDialog extends ChatDialog implements ActionListener, NetworkConfigObserver {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	
	private static final String CONFIG_TITLE = "Paramètres" ;
	private static final Dimension CONFIG_SIZE = new Dimension(300, 300) ;

	/* Some inputs for port. */
	private EnumMap <PortType, JFormattedTextField> portInputs ;
	/* Some JRadioButton for protocol choice. */
	private EnumMap <Protocol, JRadioButton> protocolButtons ;
	private Protocol oldProtocol ;
	/* Interface for broadcast choose. */
	private HashMap<InterfaceAddress, JCheckBox> interfaceBoxes ;
	/* Valid / Cancel / Default buttons. */
	private JButton validButton ;
	private JButton cancelButton ;
	private JButton defaultButton ;
	/* The controller... */
	private MessageController msgCtrl ;
	/* Some value to save. */
	private Protocol protocolSaved ;
	private EnumMap <Protocol, EnumMap <PortType, Integer>> portsSaved ;
	private Set <InterfaceAddress> interfaceSaved ;
	/* The listeners. */
	private KeyListener keyListener = new KeyAdapter () {
		@Override
		public void keyPressed (KeyEvent ke) {
			if (ke.getKeyCode() == KeyEvent.VK_ENTER) {
				ChatConfigDialog.this.validButton.doClick() ;
			}
			if (ke.getKeyCode() == KeyEvent.VK_ESCAPE) {
				ChatConfigDialog.this.cancelButton.doClick();
			}
		}
	} ;
	private FocusListener focusListener = new FocusAdapter () {
		@Override
		public void focusGained(FocusEvent a) {
			JFormattedTextField field = (JFormattedTextField)a.getSource() ;
			field.setSelectionStart(0) ;
			field.setSelectionEnd(field.getText().length()) ;
		}
	} ;
	private WindowListener windowListener = new WindowAdapter () {
		@Override
		public void windowActivated (WindowEvent a) {
			ChatConfigDialog.this.protocolSaved = getSelectedProtocol() ;
			ChatConfigDialog.this.interfaceSaved = new HashSet <InterfaceAddress> () ;
			for (InterfaceAddress ia : ChatConfigDialog.this.interfaceBoxes.keySet()) {
				if (ChatConfigDialog.this.interfaceBoxes.get(ia).isSelected()) {
					ChatConfigDialog.this.interfaceSaved.add(ia) ;
				}
			}
			for (Protocol p : Protocol.values()) {
				if (p.equals(getSelectedProtocol())) {
					ChatConfigDialog.this.portsSaved.put(p, getSelectedPorts()) ;
				}
				else {
					ChatConfigDialog.this.portsSaved.put(p, null) ;
				}
			}
		}
		
	} ;
	
	/**
	 * @return A port formatter that force user to enter a "valid" port
	 */
	private AbstractFormatter getPortFormatter () {
		DecimalFormat portFormat = new DecimalFormat() ;
		portFormat.setGroupingSize(5) ;
		portFormat.setMinimumIntegerDigits(1) ;
		NumberFormatter numf = new NumberFormatter(portFormat) ;
		numf.setAllowsInvalid(false) ;
		numf.setMaximum(65535) ;
		return numf ;
	}
	
	/**
	 * Create and return the UDP Panel with 2 inputs
	 */
	private JPanel createUDPPanel () {
		JFormattedTextField udpSendInput = new JFormattedTextField(getPortFormatter()) ;
		udpSendInput.addKeyListener(this.keyListener) ;
		udpSendInput.addFocusListener(this.focusListener) ;
		JFormattedTextField udpRecvInput = new JFormattedTextField(getPortFormatter()) ;
		udpRecvInput.addKeyListener(this.keyListener) ;
		udpRecvInput.addFocusListener(this.focusListener) ;
		this.portInputs.put(PortType.UDP_RECEIVE, udpRecvInput) ;
		this.portInputs.put(PortType.UDP_SEND, udpSendInput) ;
		JPanel udpPanel = new JPanel () ;
		udpPanel.setLayout(new GridLayout(2, 2)) ;
		udpPanel.add(new JLabel("Port d'écoute UDP : ")) ;
		udpPanel.add(udpRecvInput) ;
		udpPanel.add(new JLabel("Port d'envoi UDP : ")) ;
		udpPanel.add(udpSendInput) ;
		udpPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED),
				"Échange de signaux en UDP")) ;
		return udpPanel ;
	}
	
	/**
	 * Create and return the TCP Panel (2 inputs)
	 */
	private JPanel createTCPPanel () {
		JFormattedTextField tcpConnectInput = new JFormattedTextField(getPortFormatter()) ;
		tcpConnectInput.addKeyListener(this.keyListener) ;
		tcpConnectInput.addFocusListener(this.focusListener) ;
		JFormattedTextField tcpAcceptInput = new JFormattedTextField(getPortFormatter()) ;
		tcpAcceptInput.addKeyListener(this.keyListener) ;
		tcpAcceptInput.addFocusListener(this.focusListener) ;
		this.portInputs.put(PortType.TCP_ACCEPT, tcpAcceptInput) ;
		this.portInputs.put(PortType.TCP_CONNECT, tcpConnectInput) ;
		JPanel tcpPanel = new JPanel () ;
		tcpPanel.setLayout(new GridLayout(2, 2)) ;
		tcpPanel.add(new JLabel("Port d'écoute TCP : ")) ;
		tcpPanel.add(tcpAcceptInput) ;
		tcpPanel.add(new JLabel("Port d'envoi TCP : ")) ;
		tcpPanel.add(tcpConnectInput) ;
		tcpPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED),
				"Transfert de fichiers en TCP")) ;
		return tcpPanel ;
	}
	
	/**
	 * Create and return the buttons panel which contains "Save", "Cancel" and "Default"
	 */
	public JPanel createButtonsPanel () {
		this.validButton = new JButton("Sauvegarder") ;
		this.validButton.addActionListener(this) ;
		this.cancelButton = new JButton("Annuler") ;
		this.cancelButton.addActionListener(this) ;
		this.defaultButton = new JButton("Défaut") ;
		this.defaultButton.addActionListener(this) ;
		JPanel buttonPanel = new JPanel() ;
		buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS)) ;
		buttonPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)) ;
		buttonPanel.add(Box.createHorizontalGlue()) ;
		buttonPanel.add(this.validButton) ;
		buttonPanel.add(Box.createHorizontalGlue()) ;
		buttonPanel.add(this.cancelButton) ;
		buttonPanel.add(Box.createHorizontalGlue()) ;
		buttonPanel.add(this.defaultButton) ;
		buttonPanel.add(Box.createHorizontalGlue()) ;
		return buttonPanel ;
	}
	
	/**
	 * Create and return the full port configuration panel (this function
	 * call createUDPPanel and createTCPPanel).
	 */
	public JPanel createPortConfigPanel () {
		JPanel portPanel = new JPanel () ;
		this.portInputs = new EnumMap <PortType, JFormattedTextField> (PortType.class) ;
		portPanel.setLayout(new BoxLayout(portPanel, BoxLayout.Y_AXIS)) ;
		portPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)) ;
		portPanel.add(createUDPPanel()) ;
		portPanel.add(Box.createRigidArea(new Dimension(0, 10))) ;
		portPanel.add(createTCPPanel()) ;
		return portPanel  ;
	}
	
	/**
	 * Create all the JRadioButton corresponding to protocol.
	 */
	public void createProtocolButtonGroup () {
		ButtonGroup bGroup = new ButtonGroup() ;
		this.protocolButtons = new EnumMap <NetworkConfiguration.Protocol, JRadioButton> (NetworkConfiguration.Protocol.class) ;
		for (Protocol p : NetworkConfiguration.Protocol.values()) {
			JRadioButton jrb = new JRadioButton("Protocole " + p.toString()) ;
			jrb.addActionListener(this) ;
			bGroup.add(jrb) ;
			this.protocolButtons.put(p, jrb) ;
		}
	}
	
	/**
	 * Create and return the protocol config panel. This function create all the
	 * jradiobutton for protocol.
	 */
	public JPanel createProtocolConfigPanel () {
		JPanel portPanel = new JPanel () ;
		portPanel.setLayout(new BoxLayout(portPanel, BoxLayout.Y_AXIS)) ;
		portPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED),
				"Choix du protocole")) ;
		createProtocolButtonGroup() ;
		for (Protocol p : this.protocolButtons.keySet()) {
			portPanel.add(this.protocolButtons.get(p)) ;
		}
		return portPanel  ;
	}
	
	/**
	 * Create and return the interface manager panel
	 */
	public JScrollPane createInterfacesPanel () {
		JPanel broadcastPanel = new JPanel () ;
		broadcastPanel.setLayout(new BoxLayout(broadcastPanel, BoxLayout.Y_AXIS)) ;
		broadcastPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED),
				"Choix des adresses de diffusion")) ;
		this.interfaceBoxes = new HashMap <InterfaceAddress, JCheckBox> () ;
		for (InterfaceAddress ia : AbstractNetworkInterface.getAllLocalInterfaces()) {
			String info = ia.getAddress().getHostAddress() ;
			if (ia.getAddress().isLinkLocalAddress() || ia.getAddress().isLoopbackAddress()) {
				info += " (*)" ;
			}
			JCheckBox jcb = new JCheckBox(info) ;
			broadcastPanel.add(jcb) ;
			this.interfaceBoxes.put(ia, jcb) ;
		}
		broadcastPanel.add(new JLabel("<html>&nbsp;&nbsp;<em>(*) Loopback or LinkLocal interface</em></html>")) ;
		return new JScrollPane(broadcastPanel) ;
	}
	
	public ChatConfigDialog (Window master, MessageController msgCtrl) {
		super (master, CONFIG_TITLE, CONFIG_SIZE) ;
		this.msgCtrl = msgCtrl ;
		this.oldProtocol = this.msgCtrl.getDefaultProtocol() ;
		/* Configure the window */
		addWindowListener(this.windowListener) ;
		/* Create some attributes to save values... */
		this.portsSaved = new EnumMap <Protocol, EnumMap <PortType, Integer>> (Protocol.class) ;
		this.interfaceSaved = new HashSet <InterfaceAddress> () ;
		/* Create and add component */
		setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS)) ;
		JTabbedPane dialogPanel = new JTabbedPane () ;
		dialogPanel.setBorder(BorderFactory.createEmptyBorder()) ;
		dialogPanel.addTab("Ports", createPortConfigPanel()) ;
		dialogPanel.addTab("Protocole", createProtocolConfigPanel()) ;
		dialogPanel.addTab("Diffusion", createInterfacesPanel()) ;
		add(dialogPanel) ;
		add(createButtonsPanel()) ;
	}
	
	private Protocol getSelectedProtocol () {
		Protocol selectedProtocol = null ;
		for (Protocol p : this.protocolButtons.keySet()) {
			if (this.protocolButtons.get(p).isSelected()) {
				selectedProtocol = p ;
			}
		}
		return selectedProtocol ;
	}
	
	private EnumMap <PortType, Integer> getSelectedPorts() {
		EnumMap <PortType, Integer> ports = new EnumMap <PortType, Integer> (PortType.class) ;
		for (PortType p : this.portInputs.keySet()) {
			ports.put(p, Integer.valueOf(this.portInputs.get(p).getText())) ;
		}
		return ports ;
	}
	
	@Override
	public void actionPerformed(ActionEvent ae) {
		if (ae.getSource() instanceof JRadioButton) {
			try {
				this.oldProtocol = this.msgCtrl.getCurrentProtocol () ;
				this.msgCtrl.setProtocol(getSelectedProtocol()) ;
			} 
			catch (UserAlreadyConnected e) {
			}
		}
		else if (ae.getSource() == this.defaultButton) {
			for (Protocol p : this.protocolButtons.keySet()) {
				if (this.msgCtrl.getDefaultProtocol().equals(p)) {
					this.protocolButtons.get(p).setSelected(true) ;
					try {
						this.msgCtrl.setProtocol(getSelectedProtocol()) ;
					}
					catch (UserAlreadyConnected e) {
					}
				}
				else {
					this.protocolButtons.get(p).setSelected(false) ;
				}
			}
			for (PortType p : this.portInputs.keySet()) {
				this.portInputs.get(p).setText(String.valueOf(this.msgCtrl.getPort(p))) ;
			}
			Set <InterfaceAddress> defaultAllowed = this.msgCtrl.getDefaultAllowedInterfaces() ;
			for (InterfaceAddress ia : this.interfaceBoxes.keySet()) {
				if (defaultAllowed.contains(ia)) {
					this.interfaceBoxes.get(ia).setSelected(true) ;
				}
				else {
					this.interfaceBoxes.get(ia).setSelected(false) ;
				}
			}
		}
		else if (ae.getSource() == this.cancelButton) {
			try {
				this.oldProtocol = this.protocolSaved ; // To avoid popup
				this.msgCtrl.setProtocol(this.protocolSaved) ;
				for (Protocol protocol : this.portsSaved.keySet()) {
					if (this.portsSaved.get(protocol) != null) {
						this.msgCtrl.setPorts(protocol, this.portsSaved.get(protocol)) ;
					}
				}
				this.msgCtrl.setAllowedInterfaces(this.interfaceSaved) ;
			} 
			catch (UserAlreadyConnected e) {
			} 
			catch (BadPortException e) {
			}
			setVisible(false) ;
		}
		else {
			try {
				this.msgCtrl.setPorts(getSelectedPorts()) ;
				Set <InterfaceAddress> allowedInterfaces = new HashSet <InterfaceAddress> () ;
				for (InterfaceAddress ia : this.interfaceBoxes.keySet()) {
					if (this.interfaceBoxes.get(ia).isSelected()) {
						allowedInterfaces.add(ia) ;
					}
				}
				this.msgCtrl.setAllowedInterfaces(allowedInterfaces) ;
				setVisible(false) ;
			}
			catch (BadPortException bpe) {
				JOptionPane.showMessageDialog(this, "Un ou plusieurs ports sont incorrects.", "Port(s) incorrect(s)", JOptionPane.ERROR_MESSAGE) ;
			} 
			catch (UserAlreadyConnected e) {
			}
		}
	}

	@Override
	public void portChanged(Protocol protocol, PortType portType, int port) {
		if (getSelectedProtocol().equals(protocol)) {
			this.portInputs.get(portType).setText(String.valueOf(port)) ;
		}
	}
	
	private JLabel createUnsupportedFeaturesLabel (ArrayList <FeatureType> unsupportedFeatures) {
		String info = "<html><p>Certaines fonctionnalités ne sont pas supportées par ce protocole.<ul>" ;
		for (FeatureType ft : unsupportedFeatures) {
			info += "<li>" ;
			switch (ft) {
			case CHAT:
				info += "Messagerie" ;
				break ;
			case SUB_ROOM:
				info += "Salon à plusieurs" ;
				break ;
			case FILE_TRANSFER:
				info += "Transfert de fichiers" ;
				break ;
			}
			info += "</li>" ;
		}
		return new JLabel(info) ;
	}

	@Override
	public void protocolChanged(Protocol newProtocol,
			EnumMap<PortType, Integer> ports,
			ArrayList<FeatureType> unsupportedFeatures) {
		boolean valid = true ;
		if (this.oldProtocol != newProtocol && !unsupportedFeatures.isEmpty()) {
			if (JOptionPane.showConfirmDialog(this, createUnsupportedFeaturesLabel(unsupportedFeatures), 
					"Fonctionnalité(s) non supportée(s)", JOptionPane.YES_NO_OPTION) == JOptionPane.NO_OPTION) {
				try {
					this.msgCtrl.setProtocol(this.oldProtocol) ;
				} 
				catch (UserAlreadyConnected e) {
				}
				valid = false ;
			}
			
		}
		if (valid) {
			for (Protocol protocol : this.protocolButtons.keySet()) {
				if (protocol.equals(newProtocol)) {
					this.protocolButtons.get(protocol).setSelected(true) ;
				}
				else {
					this.protocolButtons.get(protocol).setSelected(false) ;
				}
			}
			for (PortType port : this.portInputs.keySet()) {
				this.portInputs.get(port).setText(String.valueOf(ports.get(port))) ;
			}		
			if (this.portsSaved.get(newProtocol) == null) {
				this.portsSaved.put(newProtocol, getSelectedPorts()) ;
			}
		}
	}

	@Override
	public void interfacesChanged (Set <InterfaceAddress> interfaceAddresses) {
		for (InterfaceAddress ia : this.interfaceBoxes.keySet()) {
			if (interfaceAddresses.contains(ia)) {
				this.interfaceBoxes.get(ia).setSelected(true) ;
			}
			else {
				this.interfaceBoxes.get(ia).setSelected(false) ;
			}
		}
	}

}
